﻿using System;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
using net.xBei.Configurations;
using net.xBei.Models;
using net.xBei.WebApi.Repository;
using xBei.Redis.Extension;

namespace net.xBei.WebApi.FrameworkExtensions {
    /// <summary>
    /// JWT认证扩展
    /// </summary>
    public static class JwtExtensions {
        /// <summary>
        /// 设置用户认证（Jwt）
        /// </summary>
        /// <param name="services"></param>
        /// <param name="configuration"></param>
        /// <param name="setupUserPermissionCheck"></param>
        public static void SetupJwt(this IServiceCollection services,
                                    IConfigurationRoot configuration,
                                    Action<IServiceCollection>? setupUserPermissionCheck = null) {
            services.Configure<JwtSettings>(configuration.GetSection(JwtSettings.CONFIGURATION_NAME));
            var settings = configuration.GetSection(JwtSettings.CONFIGURATION_NAME).Get<JwtSettings>();
            if (settings == null) {
                throw new ArgumentNullException(nameof(settings), "必须配置“JwtSettings”");
            }
            var redisSettings = settings.Redis;
            if (redisSettings == null) {
                throw new ArgumentNullException(nameof(redisSettings), "必须配置用来保存Token的Redis服务器");
            }
            if (string.IsNullOrWhiteSpace(redisSettings.RedisConnectionHost)) {
                throw new Exception("必须配置用来保存Token的Redis服务器地址");
            }
            var redisManager = new RedisClient(redisSettings.RedisConnectionHost,
                                   redisSettings.RedisConnectionPort,
                                   redisSettings.RedisConnectionPassword,
                                   redisSettings.RedisDb);
            services.AddSingleton<IAuthorizationHandler, BackendUserHandler>();
            services.AddSingleton<IAuthorizationHandler, SystemUserHandler>();
            //services.AddSingleton<IAuthorizationHandler, AuthorizationHandler>();
            if (setupUserPermissionCheck == null) {
                services.AddSingleton<IUserPermissionCheck, AuthorizationHandler.UserPermissionCheck>();
            } else {
                setupUserPermissionCheck.Invoke(services);
            }
            services.AddAuthorization(options => {
                options.AddPolicy(CommonConsts.Policy.Backend, policy => policy.AddRequirements(new BackendUserHandler.BackendRequirement()));
                options.AddPolicy(CommonConsts.Policy.System, policy => policy.AddRequirements(new SystemUserHandler.SystemUserRequirement()));
            });
            services.AddAuthentication(x => {
                x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            }).AddJwtBearer(options => {
                var autoRefresh = settings.AutoRefresh;
                var tokenSecret = settings.TokenSecret;
                options.Events = new JwtBearerEvents {
                    OnMessageReceived = context => {
                        //优先从header中获取token
                        context.Token = context.HttpContext.Request.Headers["Authorization"].ToString().Replace("Bearer ", "");
                        if (!string.IsNullOrWhiteSpace(context.Token)) {
                            return Task.CompletedTask;
                        }
                        context.Token = context.Request.Query["token"];
                        if (!string.IsNullOrWhiteSpace(context.Token)) {
                            return Task.CompletedTask;
                        }
                        if (!context.Request.Path.StartsWithSegments("/api", StringComparison.OrdinalIgnoreCase, out var _)) {
                            // 以api开头的请求，不从cookie中获取token
                            context.Token = context.Request.Cookies["token"];
                        }
                        return Task.CompletedTask;
                    },
                    // If token not valid(not check lifetime), return ("Token-Expired", "true")
                    // If token valid, check whether logged in(user session).
                    // check lifetime, if expired, refresh new token.
                    OnChallenge = context => {
                        context.Response.Headers.Add("Token-Expired", "true");
                        return Task.CompletedTask;
                    },

                    OnTokenValidated = async context => {
                        //context.HttpContext.Items.Add(CommonConsts.ConstName_USER_SERVICE_POINT, userServiceEndpoint);
                        context.HttpContext.AddTokenToCookie(context.SecurityToken.UnsafeToString(), context.SecurityToken.ValidTo);
                        if (IsSystemUser(context.Principal?.Identity?.Name)) {
                            return;
                        }

                        if (!await CheckUserLoginAsync(redisManager,
                                                       context.Principal?.Identity?.Name,
                                                       context.Principal?.FindFirstValue(ClaimTypes.Sid))) {
                            context.Fail("Unauthorized");
                        }
                        if (autoRefresh) {
                            // 过期（豁免期之内）自动刷新
                            if (context.SecurityToken.ValidTo < DateTime.UtcNow) {
                                try {
                                    var newToken = ApiCallerFactory.GenerateToken(context.Principal?.Claims, tokenSecret, settings.Expires);
                                    //Console.WriteLine($"newToken={newToken}");
                                    context.Response.Headers.Add("newToken", newToken);
                                    context.HttpContext.AddTokenToCookie(newToken, DateTime.UtcNow.AddSeconds(settings.Expires));
                                } catch (Exception ex) {
                                    context.Fail(ex);
                                }
                            }
                        }
                        return;
                    },
                };
                options.RequireHttpsMetadata = false;
                options.SaveToken = true;
                options.TokenValidationParameters = new TokenValidationParameters {
                    //ValidateLifetime = false, // 不验证有效期，默认是验证的
                    //ClockSkew = TimeSpan.FromSeconds(300), // 默认是五分钟
                    ValidateIssuerSigningKey = true,
                    IssuerSigningKey = new SymmetricSecurityKey(System.Text.Encoding.ASCII.GetBytes(tokenSecret)),
                    ValidateIssuer = false,
                    ValidateAudience = false
                };
            });
        }

        private static void AddTokenToCookie(this HttpContext context, string token, DateTime expires) {
            //Console.WriteLine($"cookie path = {context.Request.Path}");
            //Console.WriteLine($"cookie host = {context.Request.Host.Host}");
            if (context.Request.Path.StartsWithSegments("/api", StringComparison.OrdinalIgnoreCase, out var _)) {
                // 以 /api 开头的请求，不处理 cookie
                return;
            }
            var cookies = context.Response.Cookies;
            cookies.Append("token", token,
                          new CookieOptions {
                              Expires = expires,
                              Domain = context.Request.Host.Host,
                              SameSite = SameSiteMode.Lax,
                          });
        }
        /// <summary>
        /// 
        /// </summary>
        /// <param name="userId"></param>
        /// <returns></returns>
        public static bool IsSystemUser(string? userId) => !string.IsNullOrWhiteSpace(userId) && userId == CommonConsts.SystemUserId;
        /// <summary>
        /// 
        /// </summary>
        /// <param name="redisManager"></param>
        /// <param name="sessionId"></param>
        /// <returns></returns>
        public static async Task<string> GetUserIdBySessionIdInRedisAsync(this RedisClient redisManager, string sessionId) {
            return await redisManager.HashGetAsync($"USER:LOGIN:SESSION:{sessionId}", CommonConsts.REDISKEY_USERID, string.Empty);
        }
        /// <summary>
        /// 
        /// </summary>
        /// <param name="redisManager"></param>
        /// <param name="userId"></param>
        /// <param name="sessionId"></param>
        /// <returns></returns>
        public static async Task<bool> CheckUserLoginAsync(RedisClient redisManager, string? userId, string? sessionId) {
            return !string.IsNullOrWhiteSpace(userId)
                        && !string.IsNullOrWhiteSpace(sessionId)
                        && userId == await GetUserIdBySessionIdInRedisAsync(redisManager, sessionId);
        }
    }
}
